/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.polarrose.json;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.json.JSONArray;
import org.json.JSONObject;

import com.polarrose.json.annotations.JsonHint;
import com.polarrose.json.annotations.JsonIgnore;
import com.polarrose.json.annotations.JsonPropertyMapper;

/**
 * Deserializer used for transforming JSON string into JavaBean.
 * It supports all primitive types, it's object wrappers, arrays,
 * collections ({@link List}, {@link Set}) and {@link Map}.
 *
 * When deserializing {@link Map}, {@link List} or {@link Set} a
 * {@literal @}{@link JsonHint} annotation must be added to setter
 * of this property which includes <code>className</code> attribute
 * describing the target type of that's collection's members.
 *
 * @see JsonHint
 *
 * @author Piotr Maj
 *
 */
public class JsonDeserializer
{
    public <T> T deserialize(Class<T> clazz, JSONObject jsonObject) throws JsonDeserializationException {
        try {
			T instance = clazz.newInstance();
			deserializeObject(instance, jsonObject);
			return instance;
		} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
    }

    public <T> T deserialize(Class<T> clazz, String json) throws JsonDeserializationException {
        try {
			return deserialize(clazz, new JSONObject(json));
		} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
    }

    public <T> List<T> deserializeList(Class<T> clazz, String json) throws JsonDeserializationException {
    	try {
    		return deserializeList(clazz, new JSONArray(json));
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
    }

    //@SuppressWarnings("unchecked")
	public <T> List<T> deserializeList(Class<T> clazz, JSONArray array) throws JsonDeserializationException {
    	try {
    		List<T> returnList = new ArrayList<T>();
    		int length = array.length();
			for (int i = 0; i < length; i++) {
				T deserializedObject;
				if (isSimpleType(clazz)) {
					deserializedObject = (T) extractSimplePropertyFromArray(clazz, array, i);
				} else {
					JSONObject object = array.getJSONObject(i);
					deserializedObject = deserialize(clazz, object);
				}
    			returnList.add(deserializedObject);
    		}
    		return returnList;
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
    }

	@SuppressWarnings("unchecked")
	public <T> Map<String, T> deserializeMap(Class<T> clazz, String json) throws JsonDeserializationException {
    	try {
    		return deserializeMap(String.class, clazz, new JSONObject(json));
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
	}

	@SuppressWarnings("unchecked")
	public <S, T> Map<S, T> deserializeMap(Class<S> keyClazz, Class<T> clazz, String json) throws JsonDeserializationException {
    	try {
    		return deserializeMap(keyClazz, clazz, new JSONObject(json));
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
	}

	public <T> Map<String, T> deserializeMap(Class<T> clazz, JSONObject jsonObject) throws JsonDeserializationException {
		try {
    		return deserializeMap(String.class, clazz, jsonObject);
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
	}

	@SuppressWarnings("unchecked")
	public <S, T> Map<S, T> deserializeMap(Class<S> keyClazz, Class<T> clazz, JSONObject jsonObject) throws JsonDeserializationException {
    	try {
    		Map<S, T> returnList = new HashMap<S, T>();
    		Iterator<String> keys = jsonObject.keys();
    		while (keys.hasNext()) {
				String key = (String) keys.next();
				T deserializedObject;
				if (isSimpleType(clazz)) {
					deserializedObject = (T) extractSimpleProperty(clazz, key, jsonObject);
				} else {
					JSONObject object = jsonObject.getJSONObject(key);
					deserializedObject = deserialize(clazz, object);
				}
				
				S keyValue = createObjectFromString(keyClazz, key);
				
				returnList.put(keyValue , deserializedObject);
			}
    		return returnList;
    	} catch (JsonDeserializationException e) {
			throw e;
		} catch (Exception e) {
			throw new JsonDeserializationException("Could not deserialize object of class: " + clazz, e);
		}
	}

	private <S> S createObjectFromString(Class<S> keyClazz, String value) throws NoSuchMethodException, JsonDeserializationException, IllegalAccessException, InvocationTargetException {
		S keyValue;
		if (Date.class.isAssignableFrom(keyClazz)) {
			return (S) new Date(Long.valueOf(value));
		}
		try {
			Constructor<S> constructor = keyClazz.getConstructor(String.class);
			keyValue = constructor.newInstance(value);
		} catch (Exception e) {
			Method method = null;
			try {
				method = keyClazz.getMethod("valueOf", String.class);
			} catch (Exception e1) {
				method = keyClazz.getMethod("fromString", String.class);
			}
			if (method == null) {
				throw new JsonDeserializationException("Don't know how to convert String to " + keyClazz.getName());
			}
			keyValue = (S) method.invoke(keyClazz, value);
		}
		return keyValue;
	}

	@SuppressWarnings("unchecked")
	private <T> void deserializeObject(T instance, JSONObject jsonObject) throws Exception {
    	BeanInfo beanInfo = Introspector.getBeanInfo(instance.getClass());
    	PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    	for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
    		if(propertyDescriptor.getName().equals("class")) {
    			continue;
    		}
			Class<?> propertyType = propertyDescriptor.getPropertyType();
			String propertyName = propertyDescriptor.getName();
			Method writeMethod = propertyDescriptor.getWriteMethod();
			if (writeMethod == null) {
				continue;
			}

			// check ignored
			JsonIgnore jsonIgnore = writeMethod.getAnnotation(JsonIgnore.class);
			if (jsonIgnore != null) {
				continue;
			}

			// property mapper
			JsonPropertyMapper jsonPropertyMapper = writeMethod.getAnnotation(JsonPropertyMapper.class);
			if (jsonPropertyMapper != null) {
				propertyName = jsonPropertyMapper.name();
			}
			if (jsonObject.has(propertyName)) {
				if (isSimpleType(propertyType)) {
					Object value = extractSimpleProperty(propertyType, propertyName, jsonObject);
					writeMethod.invoke(instance, value);
				} else if (isCollection(propertyType)) {
					Collection<?> collection = instantionateCollection(propertyType);

					Class<?> elementClassName = extractGenericTypeFromWriteMethod(writeMethod);
					if (elementClassName == null) {
						elementClassName = extractTypeFromJsonHint(propertyDescriptor, propertyName);
					}

					if (!jsonObject.isNull(propertyName)) {
						writeMethod.invoke(instance, collection);
						JSONArray array = jsonObject.getJSONArray(propertyName);
						int length = array.length();
						for (int i = 0; i < length; i++) {
							Object element = null;
							if (isSimpleType(elementClassName)) {
								element = extractSimplePropertyFromArray(elementClassName, array, i);
							} else {
								element = elementClassName.newInstance();
								deserializeObject(element, array.getJSONObject(i));
							}
							propertyType.getMethod("add", Object.class).invoke(collection, element);
						}
					}
				} else if (Map.class.isAssignableFrom(propertyType)) {
					Map<Object, Object> map = new HashMap<Object, Object>();

					Class<?> elementClassName = extractGenericTypeFromWriteMethod(writeMethod);
					if (elementClassName == null) {
						elementClassName = extractTypeFromJsonHint(propertyDescriptor, propertyName);
					}
					Class<?> keyClassName = extractGenericTypeOfMapKeyFromWriteMethod(writeMethod);
					if (keyClassName == null) {
						keyClassName = extractMapKeyTypeFromJsonHint(propertyDescriptor, propertyName);
					}

					writeMethod.invoke(instance, map);
					JSONObject object = jsonObject.getJSONObject(propertyName);
					Iterator<String> keys = object.keys();
					while (keys.hasNext()) {
						String key = keys.next();
						Object element = null;
						if (isSimpleType(elementClassName)) {
							element = extractSimpleProperty(elementClassName, key, object);
						} else {
							element = elementClassName.newInstance();
							deserializeObject(element, object.getJSONObject(key));
						}
						Object keyValue = createObjectFromString(keyClassName, key);
						map.put(keyValue, element);
					}
				} else if (propertyType.isArray()) {
					if (!jsonObject.isNull(propertyName)) {
						Class<?> componentType = propertyType.getComponentType();
						JSONArray array = jsonObject.getJSONArray(propertyName);
						Object arrayToSet = Array.newInstance(componentType, array.length());
						int length = array.length();
						for (int i = 0; i < length; i++) {
							Object element = null;
							if (isSimpleType(componentType)) {
								element = extractSimplePropertyFromArray(componentType, array, i);
							} else {
								if (!array.isNull(i)) {
									element = componentType.newInstance();
									deserializeObject(element, array.getJSONObject(i));
								}
							}
							Array.set(arrayToSet, i, element);
						}
						writeMethod.invoke(instance, arrayToSet);
					} else {
						writeMethod.invoke(instance, new Object[] { null });
					}
				} else { // Java Bean
					if (!jsonObject.isNull(propertyName)) {
						if (!propertyType.isEnum()) { 
							Object element = propertyType.newInstance();
							deserializeObject(element, jsonObject.getJSONObject(propertyName));
							writeMethod.invoke(instance, element);
						} else {
							Enum[] enumConstants = (Enum[]) propertyType.getEnumConstants();
							String enumValue = jsonObject.getString(propertyName);
							for (Enum e : enumConstants) {
								if (e.name().equals(enumValue)) {
									writeMethod.invoke(instance, e);
								}
							}
						}
					} else {
						writeMethod.invoke(instance, new Object[] { null });
					}
				}
			}
		}
    	
    }

	private Class<?> extractTypeFromJsonHint(PropertyDescriptor propertyDescriptor, String propertyName) throws JsonDeserializationException {
		Class<?> elementClassName;
		JsonHint jsonHint = propertyDescriptor.getWriteMethod().getAnnotation(JsonHint.class);
		if (jsonHint == null) {
			Method readMethod = propertyDescriptor.getReadMethod();
			if (readMethod != null) {
				jsonHint = readMethod.getAnnotation(JsonHint.class);
			}
		}
		if (jsonHint == null) {
			throw new JsonDeserializationException("Don't know how to deserialize collection: " + propertyName + ". Use @JsonHint annotation.");
		}
		elementClassName = jsonHint.className();
		return elementClassName;
	}

	private Class<?> extractMapKeyTypeFromJsonHint(PropertyDescriptor propertyDescriptor, String propertyName) throws JsonDeserializationException {
		Class<?> elementClassName;
		JsonHint jsonHint = propertyDescriptor.getWriteMethod().getAnnotation(JsonHint.class);
		if (jsonHint == null) {
			Method readMethod = propertyDescriptor.getReadMethod();
			if (readMethod != null) {
				jsonHint = readMethod.getAnnotation(JsonHint.class);
			}
		}
		if (jsonHint == null) {
			throw new JsonDeserializationException("Don't know how to deserialize collection: " + propertyName + ". Use @JsonHint annotation.");
		}
		elementClassName = jsonHint.keyClassName();
		return elementClassName;
	}

	private Class<?> extractGenericTypeFromWriteMethod(Method writeMethod) {
		Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
		if (genericParameterTypes.length == 1) {
			if (genericParameterTypes[0] instanceof ParameterizedType) {
				ParameterizedType type = (ParameterizedType) genericParameterTypes[0];
				Type[] actualTypeArguments = type.getActualTypeArguments();
				if (actualTypeArguments.length == 1) { // list
					return (Class<?>) actualTypeArguments[0];
				}
				if (actualTypeArguments.length == 2) { // map
					return (Class<?>) actualTypeArguments[1];
				}
			}
		}
		return null;
	}

	private Class<?> extractGenericTypeOfMapKeyFromWriteMethod(Method writeMethod) {
		Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
		if (genericParameterTypes.length == 1) {
			if (genericParameterTypes[0] instanceof ParameterizedType) {
				ParameterizedType type = (ParameterizedType) genericParameterTypes[0];
				Type[] actualTypeArguments = type.getActualTypeArguments();
				if (actualTypeArguments.length == 2) { // map
					return (Class<?>) actualTypeArguments[0];
				}
			}
		}
		return null;
	}

	private <T> Object extractSimplePropertyFromArray(Class<T> elementClassName, JSONArray array, int index) throws Exception {
		if (array.isNull(index)) {
			return null;
		}
		if (elementClassName.equals(Boolean.class) || elementClassName.equals(boolean.class)) {
			return array.getBoolean(index);
		} else if (elementClassName.equals(Byte.class)
			|| elementClassName.equals(byte.class)) {
			return (byte) array.getLong(index);
		} else if (elementClassName.equals(Short.class)
			|| elementClassName.equals(short.class)) {
			return (short) array.getLong(index);				
		} else if (elementClassName.equals(Integer.class)
			|| elementClassName.equals(int.class)) {
			return (int) array.getLong(index);				
		} else if (elementClassName.equals(Long.class)
			|| elementClassName.equals(long.class)) {
			return array.getLong(index);
		} else if (elementClassName.equals(Float.class)
			|| elementClassName.equals(float.class)) {
			return (float) array.getDouble(index);
		} else if (elementClassName.equals(Double.class)
			|| elementClassName.equals(double.class)) {
			return array.getDouble(index);
		} else if (elementClassName.equals(Character.class)
			|| elementClassName.equals(char.class)) { 
			return array.getString(index).charAt(0);
		} else if (elementClassName.equals(String.class)) {
			return array.getString(index);
		} else if (elementClassName.equals(UUID.class)) {
			return UUID.fromString(array.getString(index));
		} else if (elementClassName.equals(URL.class)) {
			return new URL(array.getString(index));
		}

		return null;
	}

	private Collection<?> instantionateCollection(Class<?> propertyType) throws Exception {
		if (!propertyType.isInterface()) {
			return (Collection<?>) propertyType.newInstance();
		} else if (List.class.isAssignableFrom(propertyType)) {
			return new ArrayList<Object>();
		} else if (Set.class.isAssignableFrom(propertyType)) {
			return new HashSet<Object>();
		} else {
			return new ArrayList<Object>();
		}
	}

	private boolean isCollection(Class<?> propertyType) {
		return Collection.class.isAssignableFrom(propertyType);
	}

	private Object extractSimpleProperty(Class<?> propertyType, String propertyName, JSONObject jsonObject) throws Exception {
		if (jsonObject.isNull(propertyName)) {
			return null;
		} else {
			if (propertyType.equals(Boolean.class) || propertyType.equals(boolean.class)) {
				return jsonObject.getBoolean(propertyName);
			} else if (propertyType.equals(Byte.class)
				|| propertyType.equals(byte.class)) {
				return (byte) jsonObject.getLong(propertyName);
			} else if (propertyType.equals(Short.class)
				|| propertyType.equals(short.class)) {
				return (short) jsonObject.getLong(propertyName);				
			} else if (propertyType.equals(Integer.class)
				|| propertyType.equals(int.class)) {
				return (int) jsonObject.getLong(propertyName);				
			} else if (propertyType.equals(Long.class)
				|| propertyType.equals(long.class)) {
				return jsonObject.getLong(propertyName);
			} else if (propertyType.equals(Float.class)
				|| propertyType.equals(float.class)) {
				return (float) jsonObject.getDouble(propertyName);
			} else if (propertyType.equals(Double.class)
				|| propertyType.equals(double.class)) {
				return jsonObject.getDouble(propertyName);
			} else if (propertyType.equals(Character.class)
				|| propertyType.equals(char.class)) { 
				return jsonObject.getString(propertyName).charAt(0);
			} else if (propertyType.equals(String.class)) {
				return jsonObject.getString(propertyName);
			} else if (propertyType.equals(UUID.class)) {
				return UUID.fromString(jsonObject.getString(propertyName));
			} else if (propertyType.equals(URL.class)) {
				return new URL(jsonObject.getString(propertyName));
			} else if (propertyType.equals(Date.class)) {
				return new Date(jsonObject.getLong(propertyName));
			}
		}
		return null;
	}

	private boolean isSimpleType(Class<?> propertyType) {
		return propertyType.isPrimitive()
			|| propertyType.equals(Boolean.class)
			|| propertyType.equals(Byte.class)
			|| propertyType.equals(Character.class)
        	|| propertyType.equals(Short.class)
			|| propertyType.equals(Integer.class)
        	|| propertyType.equals(Long.class)
        	|| propertyType.equals(Float.class)
        	|| propertyType.equals(Double.class)
        	|| propertyType.equals(URL.class)
        	|| propertyType.equals(UUID.class)
        	|| propertyType.equals(String.class)
        	|| propertyType.equals(Date.class);
	}

}
